 /*******************************************************************************
  * Copyright (c) 2000, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  * Sebastian Davids <sdavids@gmx.de> bug 38745
  *******************************************************************************/
 package org.eclipse.ui.texteditor;

 import java.util.ArrayList ;
 import java.util.HashMap ;
 import java.util.Iterator ;
 import java.util.List ;
 import java.util.Map ;
 import java.util.ResourceBundle ;

 import org.osgi.framework.Bundle;

 import org.eclipse.swt.widgets.Shell;

 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.core.commands.operations.IOperationHistory;
 import org.eclipse.core.commands.operations.IUndoableOperation;

 import org.eclipse.core.runtime.Assert;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IAdaptable;
 import org.eclipse.core.runtime.ILog;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.core.runtime.Status;

 import org.eclipse.core.resources.IFile;
 import org.eclipse.core.resources.IMarker;
 import org.eclipse.core.resources.IResource;

 import org.eclipse.jface.dialogs.ErrorDialog;
 import org.eclipse.jface.dialogs.IInputValidator;
 import org.eclipse.jface.dialogs.InputDialog;
 import org.eclipse.jface.window.Window;

 import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IRegion;
 import org.eclipse.jface.text.Position;
 import org.eclipse.jface.text.source.IAnnotationModel;
 import org.eclipse.jface.text.source.IVerticalRuler;
 import org.eclipse.jface.text.source.IVerticalRulerInfo;

 import org.eclipse.ui.IEditorInput;
 import org.eclipse.ui.PlatformUI;
 import org.eclipse.ui.ide.undo.CreateMarkersOperation;
 import org.eclipse.ui.ide.undo.DeleteMarkersOperation;

 /**
  * A ruler action which can add and remove markers which have a visual
  * representation in the ruler.
  * <p>
  * This class may be instantiated but is not intended for sub-classing.
  * </p>
  */
 public class MarkerRulerAction extends ResourceAction implements IUpdate {

     /** The maximum length of an proposed label. */
     private static final int MAX_LABEL_LENGTH= 80;

     /** The vertical ruler info of the editor. */
     private IVerticalRulerInfo fRuler;
     /** The associated editor */
     private ITextEditor fTextEditor;
     /** The of the marker to be created/removed. */
     private String fMarkerType;
     /** The cached list of markers covering a particular vertical ruler position. */
     private List fMarkers;
     /** The flag indicating whether user interaction is required. */
     private boolean fAskForLabel;
     /** The action's resource bundle. */
     private ResourceBundle fBundle;
     /** The prefix used for resource bundle look ups. */
     private String fPrefix;
     /** The cached action label when adding a marker. */
     private String fAddLabel;
     /** The cached action label when removing a marker. */
     private String fRemoveLabel;


     /**
      * Creates a new action for the given ruler and editor. The action configures
      * its visual representation from the given resource bundle.
      *
      * @param bundle the resource bundle
      * @param prefix a prefix to be prepended to the various resource keys
      * (described in {@link org.eclipse.ui.texteditor.ResourceAction} constructor), or <code>null</code> if none
      * @param editor the editor
      * @param ruler the ruler
      * @param markerType the type of marker
      * @param askForLabel <code>true</code> if the user should be asked for a label when a new marker is created
      * @see ResourceAction#ResourceAction(ResourceBundle, String)
      * @since 2.0
      */
     public MarkerRulerAction(ResourceBundle bundle, String prefix, ITextEditor editor, IVerticalRulerInfo ruler, String markerType, boolean askForLabel) {
         super(bundle, prefix);
         Assert.isLegal(editor != null);
         
         fRuler= ruler;
         fTextEditor= editor;
         fMarkerType= markerType;
         fAskForLabel= askForLabel;

         fBundle= bundle;
         fPrefix= prefix;

         fAddLabel= getString(bundle, prefix + "add.label", prefix + "add.label"); //$NON-NLS-2$ //$NON-NLS-1$
 fRemoveLabel= getString(bundle, prefix + "remove.label", prefix + "remove.label"); //$NON-NLS-2$ //$NON-NLS-1$
 }

     /**
      * Creates a new action for the given ruler and editor. The action configures
      * its visual representation from the given resource bundle.
      *
      * @param bundle the resource bundle
      * @param prefix a prefix to be prepended to the various resource keys
      * @param ruler the ruler
      * @param editor the editor
      * @param markerType the type of the marker
      * @param askForLabel <code>true</code> if the user should be asked for a label
      * @deprecated use <code>MarkerRulerAction(ResourceBundle, String, ITextEditor, IVerticalRulerInfo, String, boolean)</code> instead
      */
     public MarkerRulerAction(ResourceBundle bundle, String prefix, IVerticalRuler ruler, ITextEditor editor, String markerType, boolean askForLabel) {
         this(bundle, prefix, editor, ruler, markerType, askForLabel);
     }


     /**
      * Returns this action's text editor.
      *
      * @return this action's text editor
      */
     protected ITextEditor getTextEditor() {
         return fTextEditor;
     }

     /**
      * Returns this action's vertical ruler.
      *
      * @return this action's vertical ruler
      * @deprecated use <code>getVerticalRulerInfo</code> instead
      */
     protected IVerticalRuler getVerticalRuler() {
         if (fRuler instanceof IVerticalRuler)
             return (IVerticalRuler) fRuler;
         return null;
     }

     /**
      * Returns this action's vertical ruler info.
      *
      * @return this action's vertical ruler info
      * @since 2.0
      */
     protected IVerticalRulerInfo getVerticalRulerInfo() {
         return fRuler;
     }

     /**
      * Returns this action's resource bundle.
      *
      * @return this action's resource bundle
      */
     protected ResourceBundle getResourceBundle() {
         return fBundle;
     }

     /**
      * Returns this action's resource key prefix.
      *
      * @return this action's resource key prefix
      */
     protected String getResourceKeyPrefix() {
         return fPrefix;
     }

     /*
      * @see IUpdate#update()
      */
     public void update() {
         //bug 38745
 int line= getVerticalRuler().getLineOfLastMouseButtonActivity() + 1;
         IDocument document= getDocument();
         if (document != null) {
             if (line > getDocument().getNumberOfLines()) {
                 setEnabled(false);
                 setText(fAddLabel);
             } else {
                 fMarkers= getMarkers();
                 setEnabled(getResource() != null && (fMarkers.isEmpty() || markersUserEditable(fMarkers)));
                 setText(fMarkers.isEmpty() ? fAddLabel : fRemoveLabel);
             }
         }
     }

     /**
      * Returns whether the given markers are all editable by the user.
      *
      * @param markers the list of markers to test
      * @return boolean <code>true</code> if they are all editable
      * @since 3.2
      */
     private boolean markersUserEditable(List markers) {
         Iterator iter= markers.iterator();
         while (iter.hasNext()) {
             if (!isUserEditable((IMarker)iter.next()))
                 return false;
         }
         return true;
     }

     /**
      * Returns whether the given marker is editable by the user.
      *
      * @param marker the marker to test
      * @return boolean <code>true</code> if it is editable
      * @since 3.2
      */
     private boolean isUserEditable(IMarker marker) {
         return marker != null && marker.exists() && marker.getAttribute(IMarker.USER_EDITABLE, true);
     }

     /*
      * @see Action#run()
      */
     public void run() {
         if (fMarkers.isEmpty())
             addMarker();
         else
             removeMarkers(fMarkers);
     }

     /**
      * Returns the resource for which to create the marker,
      * or <code>null</code> if there is no applicable resource.
      *
      * @return the resource for which to create the marker or <code>null</code>
      */
     protected IResource getResource() {
         IEditorInput input= fTextEditor.getEditorInput();

         IResource resource= (IResource) input.getAdapter(IFile.class);

         if (resource == null)
             resource= (IResource) input.getAdapter(IResource.class);

         return resource;
     }

     /**
      * Returns the <code>AbstractMarkerAnnotationModel</code> of the editor's input.
      *
      * @return the marker annotation model
      */
     protected AbstractMarkerAnnotationModel getAnnotationModel() {
         IDocumentProvider provider= fTextEditor.getDocumentProvider();
         IAnnotationModel model= provider.getAnnotationModel(fTextEditor.getEditorInput());
         if (model instanceof AbstractMarkerAnnotationModel)
             return (AbstractMarkerAnnotationModel) model;
         return null;
     }

     /**
      * Returns the <code>IDocument</code> of the editor's input.
      *
      * @return the document of the editor's input
      */
     protected IDocument getDocument() {
         IDocumentProvider provider= fTextEditor.getDocumentProvider();
         return provider.getDocument(fTextEditor.getEditorInput());
     }

     /**
      * Checks whether a position includes the ruler's line of activity.
      *
      * @param position the position to be checked
      * @param document the document the position refers to
      * @return <code>true</code> if the line is included by the given position
      */
     protected boolean includesRulerLine(Position position, IDocument document) {

         if (position != null) {
             try {
                 int markerLine= document.getLineOfOffset(position.getOffset());
                 int line= fRuler.getLineOfLastMouseButtonActivity();
                 if (line == markerLine)
                     return true;
                 // commented because of "1GEUOZ9: ITPJUI:ALL - Confusing UI for multi-line Bookmarks and Tasks"
 // return (markerLine <= line && line <= document.getLineOfOffset(position.getOffset() + position.getLength()));
 } catch (BadLocationException x) {
             }
         }

         return false;
     }

     /**
      * Handles core exceptions. This implementation logs the exceptions
      * with the workbench plug-in and shows an error dialog.
      *
      * @param exception the exception to be handled
      * @param message the message to be logged with the given exception
      */
     protected void handleCoreException(CoreException exception, String message) {
         Bundle bundle= Platform.getBundle(PlatformUI.PLUGIN_ID);
         ILog log= Platform.getLog(bundle);

         if (message != null)
             log.log(new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, IStatus.OK, message, exception));
         else
             log.log(exception.getStatus());


         Shell shell= getTextEditor().getSite().getShell();
         String title= getString(fBundle, fPrefix + "error.dialog.title", fPrefix + "error.dialog.title"); //$NON-NLS-2$ //$NON-NLS-1$
 String msg= getString(fBundle, fPrefix + "error.dialog.message", fPrefix + "error.dialog.message"); //$NON-NLS-2$ //$NON-NLS-1$

         ErrorDialog.openError(shell, title, msg, exception.getStatus());
     }

     /**
      * Returns all markers which include the ruler's line of activity.
      *
      * @return all a list of markers which include the ruler's line of activity
      */
     protected List getMarkers() {

         List markers= new ArrayList ();

         IResource resource= getResource();
         IDocument document= getDocument();
         AbstractMarkerAnnotationModel model= getAnnotationModel();

         if (resource != null && model != null && resource.exists()) {
             try {
                 IMarker[] allMarkers= resource.findMarkers(fMarkerType, true, IResource.DEPTH_ZERO);
                 if (allMarkers != null) {
                     for (int i= 0; i < allMarkers.length; i++) {
                         if (includesRulerLine(model.getMarkerPosition(allMarkers[i]), document)) {
                             markers.add(allMarkers[i]);
                         }
                     }
                 }
             } catch (CoreException x) {
                 handleCoreException(x, TextEditorMessages.MarkerRulerAction_getMarker);
             }
         }

         return markers;
     }

     /**
      * Creates a new marker according to the specification of this action and
      * adds it to the marker resource.
      */
     protected void addMarker() {
         IResource resource= getResource();
         if (resource == null)
             return;
         Map attributes= getInitialAttributes();
         if (fAskForLabel) {
             if (!askForLabel(attributes))
                 return;
         }
         execute(new CreateMarkersOperation(fMarkerType, attributes, resource, getOperationName()));
     }

     /**
      * Removes the given markers.
      *
      * @param markers the markers to be deleted
      */
     protected void removeMarkers(final List markers) {
         IMarker[] markersArray= (IMarker[])markers.toArray(new IMarker[markers.size()]);
         execute(new DeleteMarkersOperation(markersArray, getOperationName()));
     }

     /**
      * Asks the user for a marker label. Returns <code>true</code> if a label
      * is entered, <code>false</code> if the user cancels the input dialog.
      * Sets the value of the attribute <code>message</code> in the given
      * map of attributes.
      *
      * @param attributes the map of attributes
      * @return <code>true</code> if the map of attributes has successfully been initialized
      */
     protected boolean askForLabel(Map attributes) {

         Object o= attributes.get("message"); //$NON-NLS-1$
 String proposal= (o instanceof String ) ? (String ) o : ""; //$NON-NLS-1$
 if (proposal == null)
             proposal= ""; //$NON-NLS-1$

         String title= getString(fBundle, fPrefix + "add.dialog.title", fPrefix + "add.dialog.title"); //$NON-NLS-2$ //$NON-NLS-1$
 String message= getString(fBundle, fPrefix + "add.dialog.message", fPrefix + "add.dialog.message"); //$NON-NLS-2$ //$NON-NLS-1$
 IInputValidator inputValidator= new IInputValidator() {
             public String isValid(String newText) {
                 return (newText == null || newText.trim().length() == 0) ? " " : null; //$NON-NLS-1$
 }
         };
         InputDialog dialog= new InputDialog(fTextEditor.getSite().getShell(), title, message, proposal, inputValidator);

         String label= null;
         if (dialog.open() != Window.CANCEL)
             label= dialog.getValue();

         if (label == null)
             return false;

         label= label.trim();
         if (label.length() == 0)
             return false;

         MarkerUtilities.setMessage(attributes, label);
         return true;
     }

     /**
      * Returns the attributes with which a newly created marker will be
      * initialized.
      *
      * @return the initial marker attributes
      */
     protected Map getInitialAttributes() {

         Map attributes= new HashMap (11);

         IDocumentProvider provider= fTextEditor.getDocumentProvider();
         IDocument document= provider.getDocument(fTextEditor.getEditorInput());
         int line= fRuler.getLineOfLastMouseButtonActivity();
         int start= -1;
         int end= -1;
         int length= 0;

         try {

             IRegion lineInformation= document.getLineInformation(line);
             start= lineInformation.getOffset();
             length= lineInformation.getLength();

             end= start + length;


         } catch (BadLocationException x) {
         }

         // marker line numbers are 1-based
 MarkerUtilities.setMessage(attributes, getLabelProposal(document, start, length));
         MarkerUtilities.setLineNumber(attributes, line + 1);
         MarkerUtilities.setCharStart(attributes, start);
         MarkerUtilities.setCharEnd(attributes, end);

         return attributes;
     }

     /**
      * Returns the initial label for the marker.
      *
      * @param document the document from which to extract a label proposal
      * @param offset the document offset of the range from which to extract the label proposal
      * @param length the length of the range from which to extract the label proposal
      * @return the label proposal
      * @since 3.0
      */
     protected String getLabelProposal(IDocument document, int offset, int length) {
         try {
             String label= document.get(offset, length).trim();
             if (label.length() <= MAX_LABEL_LENGTH)
                 return label;
             return label.substring(0, MAX_LABEL_LENGTH);
         } catch (BadLocationException x) {
             // don't propose label then
 return null;
         }
     }

     /**
      * Returns the name to be used for the operation.
      *
      * @return the operation name
      * @since 3.3
      */
     private String getOperationName() {
         String name= getText();
         return name == null ? TextEditorMessages.AddMarkerAction_addMarker : name;
     }

     /**
      * Execute the specified undoable operation.
      *
      * @param operation the operation to execute
      * @since 3.3
      */
     private void execute(IUndoableOperation operation) {
         final Shell shell= getTextEditor().getSite().getShell();
         IAdaptable context= new IAdaptable() {
             public Object getAdapter(Class adapter) {
                 if (adapter == Shell.class)
                     return shell;
                 return null;
             }
         };

         IOperationHistory operationHistory= PlatformUI.getWorkbench().getOperationSupport().getOperationHistory();
         try {
             operationHistory.execute(operation, null, context);
         } catch (ExecutionException e) {
             Bundle bundle= Platform.getBundle(PlatformUI.PLUGIN_ID);
             ILog log= Platform.getLog(bundle);
             String msg= getString(fBundle, fPrefix + "error.dialog.message", fPrefix + "error.dialog.message"); //$NON-NLS-2$ //$NON-NLS-1$
 log.log(new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, IStatus.OK, msg, e));
         }
     }
 }

